<?php
use router\Context;

/**
 * Routes through given path, and call appropriate method of destination class.
 * Only public methods of destination class will be called, if method was not found, called will be _handleAction($pathElement1,...) of that class.
 * You can set custom class instance initialization by calling \Router::setInitializer().
 * 
 * @author Jakub Dykowski
 *
 */
final class Router {
	
	/**
	 * Current context stack
	 *
	 * @var array
	 */
	private static $contextStack = array ();
	private static $linked;
	
	/**
	 * Custom initialization handler for making controller instances.
	 *
	 * @var callable
	 */
	private static $customControllerInitializer;

	/**
	 * Do NOT use
	 */
	private function __construct() {}

	/**
	 * Argument can be string (a path) or Context object
	 *
	 * @param mixed $pathOrContext        	
	 * @throws InvalidArgumentException if $pathOrContext is empty, isn't string nor Context, or is just not valid
	 * @return mixed
	 */
	static function route($pathOrContext) {
		if ($pathOrContext == null)
			throw new InvalidArgumentException("pathOrContext cannot be empty/null");
		
		if ($pathOrContext instanceof Context) {
			// nothing, everything is ok
		} else if (is_string($pathOrContext))
			$pathOrContext = new Context($pathOrContext);
		else
			throw new InvalidArgumentException("pathOrContext has to be string or Context instance");
		
		array_push(self::$contextStack, $pathOrContext);
		$result = self::runContext($pathOrContext);
		array_pop(self::$contextStack);
		return $result;
	}

	/**
	 * Runs nested controller if any.
	 * Return nested controller output.
	 *
	 * @param int $consumeElements
	 *        	number of elements to not forward (first is action, then args)
	 * @return mixed probably be string, nested controller output
	 */
	static function routeForward($consumeElements = 0) {
		$context = self::getCurrentContext();
		try {
			if ($consumeElements > 0) {
				// deletes elements from context url
				$url = self::cutUrl($context->getUrl(), $context->getPosition() + 1, $consumeElements);
				$forwardedContext = new Context($url, $context->getPosition() + 1, $context->getOffset());
			} else {
				$forwardedContext = new Context($context->getUrl(), $context->getPosition() + 1, $context->getOffset());
			}
		} catch (InvalidArgumentException $e) {
			if (DEBUG)
				echo "invalid forward: $e";
			self::drawPageNotFound();
		}
		return self::route($forwardedContext);
	}

	/**
	 * Runs nested controller if any.
	 * Return nested controller output.
	 *
	 * @param unknown $subController        	
	 * @param number $consumeElements        	
	 * @return mixed
	 */
	static function routeForwardTo($subController) {
		if (strpos($subController, '/') === 0)
			throw new Exception("subController name cannot start with '/'");
		
		if (DEBUG)
			echo ">forwarding to: $subController <br>";
		
		$context = self::getCurrentContext();
		try {
			$skip = $context->getPosition();
			$prefix = $context->getUrl();
			$forwardedContext = new Context($context->getUrl() . '/' . $subController, $context->getPosition() + 1, $context->getOffset());
			return self::route($forwardedContext);
		} catch (InvalidArgumentException $e) {
			if (DEBUG)
				echo "invalid forward: $e";
			self::drawPageNotFound();
		}
	}

	/**
	 * Executes given controller.
	 * Can be called recursively (but rationally).
	 *
	 * @param string $controllerPath
	 *        	url path
	 * @param string $relative        	
	 * @throws Exception
	 */
	static function linkTo($controllerPath, $relative = false) {
		if (DEBUG)
			echo ">linking to: $controllerPath<br>";
		if (is_object($controllerPath))
			$controllerPath = get_class($controllerPath);
		
		$context = self::getCurrentContext();
		self::setLinked($context, true);
		$url = $context->getUrl();
		$elements = explode("/", $url);
		
		if ($relative) {
			$elements[(int) $context->getPosition()] = $controllerPath;
		} else {
			$elements[(int) $context->getPosition()] = $controllerPath;
			$elements = array_slice($elements, $context->getPosition(), count($elements) - $context->getPosition());
		}
		
		$newContext = new Context(implode("/", $elements), $relative ? $context->getPosition() : 0, $context->getOffset());
		return self::route($newContext);
	}

	/**
	 *
	 * @param int $consumeElements
	 *        	number of elements to consume (the first is action, then args)
	 * @return mixed, propably string, controller output
	 */
	static function redirectForward($consumeElements = 0) {
		$context = self::getCurrentContext();
		$skip = $context->getPosition() + $consumeElements + 1;
		$newContext = new Context($context->getUrl(), $skip, $skip);
		return self::route($newContext);
	}

	private static function cutUrl($url, $from, $length) {
		$parts = explode("/", $url);
		if ($length == 0)
			$length = count($parts) - $from;
		for($i = $from; $i < ($from + $length); $i++) {
			unset($parts[$i]);
		}
		return implode("/", $parts);
	}

	/**
	 * Return current context that you are now inside.
	 * If called from nester controller then return the most specific context.
	 *
	 * @return router\Context current context or null if none
	 */
	static function getCurrentContext() {
		return self::$contextStack[count(self::$contextStack) - 1];
	}

	/**
	 * Return global context for the request.
	 *
	 * @return router\Context global context or null if none
	 */
	static function getGlobalContext() {
		return self::$contextStack[0];
	}

	/**
	 * Argument $func is a function that handles destination class instance initialization.
	 * The only arguments of $func is the class to make instance of, which the $func should return, or exception will be thrown.
	 * Will be called only when the class to make instance of exists.
	 *
	 *
	 * @param callable $func
	 *        	initializer
	 * @throws InvalidArgumentException if $func is not callable
	 */
	static function setInitializator($func) {
		if (!is_callable($func, true))
			throw new InvalidArgumentException('$func has to be callable');
		self::$customControllerInitializer = $func;
	}

	/**
	 *
	 * @param Context $context
	 *        	context to run
	 *        	
	 * @return mixed controller result
	 */
	private static function runContext($context) {
		if (DEBUG) {
			echo '<br>You are in: ' . $context . "<br>";
			printf("controller: %s<br>", $context->getController());
			printf("action: %s<br>", $context->getAction());
			printf("arguments: %s<br>", print_r($context->getArguments(), true));
		}
		
		$encodedControllerName = R_CONTROLLER_PATH . $context->getController();
		$controllerClassName = self::decodeControllerName($encodedControllerName);
		
		$decodedAction = self::decodeActionName($context->getAction());
		
		if (DEBUG) {
			echo "decoded action: " . $decodedAction . "<br>";
			echo "decoded controller class: " . $controllerClassName . "<br>";
		}
		
		$result = self::runController($controllerClassName, $decodedAction, $context->getArguments());
		return $result;
	}

	/**
	 *
	 * @param string $className        	
	 * @param string $action        	
	 * @param array $args        	
	 * @return mixed
	 */
	private static function runController($className, $action, $args) {
		// make sure controller class exists
		if (!class_exists($className) || self::isClassAbstract($className)) {
			if (DEBUG)
				echo "controller $className not found (with namespace)<br>";
			if (R_ONLY_NAMESPACE_SUPPORT || !self::loadControllerClassWihtoutNamespace($className)) {
				if (DEBUG)
					echo "controller $className not found (without namespace)<br>";
					
					// TODO catch eventual errors
					// run sub controllers
				if ($action != null)
					return self::routeForward();
				throw new RoutingException("controller ''$className'' expected, but not found");
			} else {
				$parts = explode("\\", $className);
			}
			$className = $parts[count($parts) - 1];
		}
		
		if (DEBUG)
			echo "running controller: $className<br><hr>";
			
		// run controller
		if (self::$customControllerInitializer === null)
			$controller = new $className();
		else {
			$initializer = self::$customControllerInitializer;
			$controller = $initializer($className);
			if (!($controller instanceof $className))
				throw new LogicException("router's custom controller initializer supposed to return instance of `$className`, but instead `" . ($className != null ? get_class($controller) : 'null'). "` was returned") ;
		}
		if (strpos($action, "_") !== 0 && method_exists($controller, $action)) {
			$result = call_user_func_array(array (
					&$controller,
					$action 
			), $args);
		} else if (method_exists($controller, "_handleAction")) {
			// prepend action as first args
			array_unshift($args, $action);
			$result = call_user_func_array(array (
					&$controller,
					'_handleAction' 
			), $args);
		} else {
			if (DEBUG)
				echo "action '$action' not found";
			self::drawPageNotFound();
		}
		return $result;
	}

	/**
	 * Draws default 'page not found' page and stops execution of script.
	 */
	static function drawPageNotFound() {
		throw new RoutingException("page '" . self::getCurrentContext()->getUrl() . "' not found");
	}

	/**
	 *
	 * @param Context $context        	
	 * @param bool $redirected        	
	 */
	private static function setLinked($context, $redirected) {
		if ($context instanceof Context)
			self::$linked[$context->getUrl()] = $redirected;
	}

	/**
	 *
	 * @param Context $context        	
	 */
	private static function isLinked($context) {
		// suppress warning about index not found, it means that context is not linked, so its ok
		return @self::$linked[$context->getUrl()];
	}

	private static function encodeControllerName($name) {
		$encoded = preg_replace('/([a-zA-Z])?([A-Z])/e', function ($m) {
			return $m[1] . ($m[1] ? '_' : '') . strtolower($m[2]);
		}, $name);
		return $encoded;
	}

	private static function decodeControllerName($encodedName) {
		$parts = explode("/", $encodedName);
		$parts[count($parts) - 1] = preg_replace_callback('/(^|(\/)|_)([a-zA-Z])([^_\/]*?)/', function (&$m) {
			return "$m[2]" . strtoupper($m[3]) . "$m[4]$m[5]";
		}, $parts[count($parts) - 1]);
		return implode('\\', $parts);
	}

	private static function decodeActionName($name) {
		return preg_replace_callback('/_([a-z])/', function (&$match) {
			return strtoupper($match[1]);
		}, $name);
	}

	private static function isClassAbstract($className) {
		$class = new ReflectionClass($className);
		return $class->isAbstract();
	}

	private static function loadControllerClassWihtoutNamespace($class) {
		$parts = explode('\\', $class);
		$className = $parts[count($parts) - 1];
		$path = implode("/", array_slice($parts, 0, count($parts) - 1));
		$filepath = R_CLASS_PATH . R_CONTROLLER_PATH . $path . "/" . $className . ".php";
		if (file_exists($filepath))
			include_once $filepath;
		return class_exists($className) && !self::isClassAbstract($className);
	}
}
